﻿/* 
 * Copyright 2010 Justin Pealing
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. 
 */

using System;
using System.IO;
using System.Collections.Generic;

namespace Minnow.Web.FastCGI
{
    /// <summary>
    /// Encapsulates a single request to a FastCGI application
    /// </summary>
    public class FastCGIRequest
    {
        private BufferedConnection connection;
        private bool waitingForEnd = false;
        private object waitForEndLock = new object();

        /// <summary>
        /// Initialises a new instance of <see cref="FastCGIRequest" />.
        /// </summary>
        /// <param name="Connection">FastCGI connection to use.</param>
        /// <param name="Owner">FastCGI process to which this request belongs to.</param>
        /// <param name="RequestId">Identifies the FastCGI request to which the record belongs.</param>
        internal FastCGIRequest(FastCGIProcess Owner, BufferedConnection Connection, Role Role, int RequestId)
        {
            this.owner = Owner;
            this.role = Role;
            this.requestId = RequestId;
            this.connection = Connection;
            this.fastCGIData = new FastCGIStream(this, Connection, RecordType.FCGI_DATA);
            this.standardInput = new FastCGIStream(this, Connection, RecordType.FCGI_STDIN);
            this.standardOutput = new FastCGIStream(this, Connection, RecordType.FCGI_STDOUT);
            this.standardError = new FastCGIStream(this, Connection, RecordType.FCGI_STDERR);
        }

        /// <summary>
        /// The role the Web server expects the application to play.
        /// </summary>
        public Role Role
        {
            get
            {
                return this.role;
            }
        }
        private Role role;

        /// <summary>
        /// Gets the role-specific app status.
        /// </summary>
        /// <exception cref="InvalidOperationException">Request has not yet ended.</exception>
        public int AppStatus
        {
            get
            {
                if (!this.RequestEnded)
                {
                    throw new InvalidOperationException("Request has not yet ended");
                }
                return this.appStatus;
            }
        }
        private int appStatus;

        /// <summary>
        /// Gets the a protocol-level status code.
        /// </summary>
        /// <exception cref="InvalidOperationException">Request has not yet ended.</exception>
        public ProtocolStatus EndReason
        {
            get
            {
                if (!this.RequestEnded)
                {
                    throw new InvalidOperationException("Request has not yet ended");
                }
                return this.endReason;
            }
        }
        private ProtocolStatus endReason;

        /// <summary>
        /// Returns true if the request has ended, false otherwise.
        /// </summary>
        public bool RequestEnded
        {
            get
            {
                if (this.requestEnded)
                {
                    return true;
                }
                if (this.waitingForEnd)
                {
                    return false;
                }

                lock (this.waitForEndLock)
                {
                    if (this.waitingForEnd)
                    {
                        return false;
                    }

                    Record? record = this.connection.GetBufferedRecord(this.requestId, RecordType.FCGI_END_REQUEST);
                    if (record == null)
                    {
                        return false;
                    }
                    else
                    {
                        this.processEndRequestRecord(record.Value);
                        return true;
                    }
                }
            }
        }
        private bool requestEnded;

        /// <summary>
        /// FastCGI process to which this request belongs to.
        /// </summary>
        public FastCGIProcess Owner
        {
            get
            {
                return this.owner;
            }
        }
        private FastCGIProcess owner;

        /// <summary>
        /// Identifies the FastCGI request to which the record belongs.
        /// </summary>
        public int RequestId
        {
            get
            {
                return this.requestId;
            }
        }
        private int requestId;

        /// <summary>
        /// Gets a writable stream that can be used to send data to the FastCGI standard 
        /// input stream of the child FastCGI application.
        /// </summary>
        public Stream StandardInput
        {
            get
            {
                return this.standardInput;
            }
        }
        private FastCGIStream standardInput;

        /// <summary>
        /// Gets a writable stream that can be used to send data to the FastCGI data stream
        /// of the child FastCGI application.
        /// </summary>
        public Stream FastCgiData
        {
            get
            {
                return this.fastCGIData;
            }
        }
        private FastCGIStream fastCGIData;

        /// <summary>
        /// Gets a readable stream that can be used to read data from the FastCGI standard
        /// output stream of the child FastCGI application.
        /// </summary>
        public Stream StandardOutput
        {
            get
            {
                return this.standardOutput;
            }
        }
        private FastCGIStream standardOutput;

        /// <summary>
        /// Gets a readable stream that can be used to read data from the FastCGI standard
        /// error stream of the child FastCGI application.
        /// </summary>
        public Stream StandardError
        {
            get
            {
                return this.standardError;
            }
        }
        private FastCGIStream standardError;

        /// <summary>
        /// Sends a collection of name-value pair to the FastCGI child process.
        /// </summary>
        /// <param name="Params">Parameters to send, or null to send an empty params record.</param>
        public void SendParams(IEnumerable<KeyValuePair<string, string>> Params)
        {
            this.connection.Connection.SendParams(this.RequestId, Params);
        }

        /// <summary>
        /// Sends an abort request record to the FastCGI child process.
        /// </summary>
        public void AbortRequest()
        {
            this.connection.Connection.AbortRequest(this.RequestId);
        }

        /// <summary>
        /// Waits for the end of the request.
        /// </summary>
        /// <returns>The request status.</returns>
        public ProtocolStatus WaitForRequestEnd()
        {
            if (this.RequestEnded)
            {
                return this.EndReason;
            }

            lock (this.waitForEndLock)
            {
                this.waitingForEnd = true;
            }

            if (this.RequestEnded)
            {
                return this.EndReason;
            }

            this.processEndRequestRecord(this.connection.ReadRecord(this.requestId, RecordType.FCGI_END_REQUEST));
            return this.EndReason;
        }

        /// <summary>
        /// Processes an end request record, setting the AppStatus, ProtocolStatus and RequestEnded values.
        /// </summary>
        /// <param name="Record">Record to process.</param>
        private void processEndRequestRecord(Record Record)
        {
            EndRequestBody endRequest = new EndRequestBody(Record.ContentData);
            this.appStatus = endRequest.AppStatus;
            this.endReason = endRequest.ProtocolStatus;
            this.requestEnded = true;
        }
    }
}
